﻿import express from "express";
import cors from "cors";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import multer from "multer";
import path from "path";
import fs from "fs";
import { nanoid } from "nanoid";
import { query, execute, transaction } from "./db.js";

const app = express();
app.use(cors());
app.use(express.json());

// 设置响应字符集为 UTF-8
app.use((req, res, next) => {
  res.setHeader('Content-Type', 'application/json; charset=utf-8');
  next();
});

const uploadDir = path.resolve(process.cwd(), "uploads");
fs.mkdirSync(uploadDir, { recursive: true });

const storage = multer.diskStorage({
  destination: (_req, _file, cb) => cb(null, uploadDir),
  filename: (_req, file, cb) => {
    const ext = path.extname(file.originalname || "").toLowerCase();
    const name = `${Date.now()}-${nanoid(6)}${ext}`;
    cb(null, name);
  },
});

const upload = multer({
  storage,
  limits: {
    fileSize: Number(process.env.UPLOAD_MAX_BYTES || 5 * 1024 * 1024),
  },
  fileFilter: (_req, file, cb) => {
    if (file.mimetype?.startsWith("image/")) {
      cb(null, true);
    } else {
      cb(new multer.MulterError("LIMIT_UNEXPECTED_FILE", file.fieldname));
    }
  },
});

app.use("/uploads", express.static(uploadDir));

const port = process.env.PORT || 3000;
const JWT_SECRET = process.env.JWT_SECRET || "mint-health-secret";
const TOKEN_EXPIRES_IN = "2h";

const sanitizeUser = (user) => ({
  id: user.id,
  email: user.email,
  name: user.name,
  role: user.role,
});

const issueToken = (user) =>
  jwt.sign(
    {
      sub: user.id,
      role: user.role,
      email: user.email,
      name: user.name,
    },
    JWT_SECRET,
    { expiresIn: TOKEN_EXPIRES_IN },
  );

const authRequired = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith("Bearer ")) {
    return res.status(401).json({ message: "Unauthorized" });
  }

  const token = authHeader.slice(7);
  try {
    const payload = jwt.verify(token, JWT_SECRET);
    req.user = payload;
    return next();
  } catch (error) {
    return res.status(401).json({ message: "Invalid or expired token" });
  }
};

const adminOnly = (req, res, next) => {
  if (req.user?.role !== "admin") {
    return res.status(403).json({ message: "Admin privileges required" });
  }
  return next();
};

const ensureArray = (value) => {
  if (!value) return [];
  if (Array.isArray(value)) return value;
  if (typeof value === "string") {
    try {
      const parsed = JSON.parse(value);
      return Array.isArray(parsed) ? parsed : [];
    } catch (error) {
      return [];
    }
  }
  return [];
};

const normalizeIngredients = (value) =>
  ensureArray(value)
    .map((item) => ({
      name: String(item?.name ?? "").trim(),
      amount: String(item?.amount ?? "").trim(),
    }))
    .filter((item) => item.name);

const prepareIngredients = (value) =>
  ensureArray(value)
    .map((item) => ({
      name: String(item?.name ?? "").trim(),
      amount: String(item?.amount ?? "").trim(),
    }))
    .filter((item) => item.name);

const normalizeTips = (value) => ensureArray(value).map((tip) => String(tip).trim()).filter(Boolean);

const computeBodyMetrics = ({ height, weight, gender, age }) => {
  const h = Number(height) || 0;
  const w = Number(weight) || 0;
  const a = Number(age) || 0;
  const bmi = h > 0 ? Number((w / ((h / 100) ** 2)).toFixed(2)) : null;
  if (!w || !h || !a || !gender) {
    return { bmi, bmr: null };
  }
  const isMale = gender === 'male';
  const bmr = isMale
    ? Math.round(10 * w + 6.25 * h - 5 * a + 5)
    : Math.round(10 * w + 6.25 * h - 5 * a - 161);
  return { bmi, bmr };
};

const getUserProfile = async (userId) => {
  const rows = await query(
    `SELECT
       user_id AS userId,
       height,
       weight,
       gender,
       age,
       bmi,
       bmr,
       goal,
       DATE_FORMAT(updated_at, "%Y-%m-%d %H:%i:%s") AS updatedAt
     FROM user_profiles
     WHERE user_id = :userId`,
    { userId },
  );
  return rows[0] ?? null;
};

const ensureUserProfile = async (userId) => {
  const existing = await getUserProfile(userId);
  if (existing) {
    return existing;
  }
  await execute(
    `INSERT INTO user_profiles (user_id, height, weight, gender, age, bmi, bmr, goal)
     VALUES (:userId, 0, 0, 'female', NULL, NULL, NULL, '记录你的健康目标')`,
    { userId },
  );
  return getUserProfile(userId);
};

const saveUserProfile = async (userId, payload) => {
  const { height, weight, gender, age, goal } = payload;
  const metrics = computeBodyMetrics({ height, weight, gender, age });
  const goalText = goal ?? '记录你的健康目标';
  await execute(
    `INSERT INTO user_profiles (user_id, height, weight, gender, age, bmi, bmr, goal)
       VALUES (:userId, :height, :weight, :gender, :age, :bmi, :bmr, :goal)
     ON DUPLICATE KEY UPDATE
       height = VALUES(height),
       weight = VALUES(weight),
       gender = VALUES(gender),
       age = VALUES(age),
       bmi = VALUES(bmi),
       bmr = VALUES(bmr),
       goal = VALUES(goal)`,
    { userId, height: height ?? null, weight: weight ?? null, gender: gender ?? null, age: age ?? null, bmi: metrics.bmi, bmr: metrics.bmr, goal: goalText },
  );
  return getUserProfile(userId);
};

const fetchArticles = async () =>
  query(
    `SELECT id, title, summary, body, cover_image AS coverImage, category,
            DATE_FORMAT(created_at, "%Y-%m-%d %H:%i:%s") AS createdAt
       FROM articles ORDER BY created_at DESC`,
  );

const fetchRecipes = async () => {
  const [recipes, tags] = await Promise.all([
    query(
      'SELECT id, name, calories, image, ingredients, method, DATE_FORMAT(created_at, "%Y-%m-%d %H:%i:%s") AS createdAt FROM recipes ORDER BY created_at DESC',
    ),
    query('SELECT recipe_id, tag FROM recipe_tags'),
  ]);

  const tagMap = tags.reduce((acc, record) => {
    if (!acc[record.recipe_id]) acc[record.recipe_id] = [];
    acc[record.recipe_id].push(record.tag);
    return acc;
  }, {});

  return recipes.map((recipe) => ({
    id: recipe.id,
    name: recipe.name,
    calories: recipe.calories,
    image: recipe.image,
    createdAt: recipe.createdAt,
    ingredients: normalizeIngredients(recipe.ingredients),
    method: recipe.method ?? "",
    tags: tagMap[recipe.id] ?? [],
  }));
};

const fetchGuides = async () =>
  query('SELECT id, title, icon, image, tips FROM guides ORDER BY created_at DESC').then((rows) =>
    rows.map((guide) => ({
      ...guide,
      image: guide.image ?? '',
      tips: normalizeTips(guide.tips),
    })),
  );

// BMI 计算：体重(kg) / (身高(m))^2
const calculateBMI = (weight, height) => {
  if (!weight || !height || height <= 0) return 0;
  const heightInMeters = height / 100;
  return Math.round((weight / (heightInMeters * heightInMeters)) * 10) / 10;
};

// 基础代谢率计算 (Mifflin-St Jeor 公式)
// 男性：10 × 体重(kg) + 6.25 × 身高(cm) - 5 × 年龄 + 5
// 女性：10 × 体重(kg) + 6.25 × 身高(cm) - 5 × 年龄 - 161
const calculateBMR = (weight, height, age, gender) => {
  if (!weight || !height || !age) return 0;
  const base = 10 * weight + 6.25 * height - 5 * age;
  return Math.round(gender === 'female' ? base - 161 : base + 5);
};



app.get('/api/health', (_req, res) => {
  res.json({ status: 'ok', service: 'mint-health-api', db: 'mysql' });
});

app.post('/api/media/upload', authRequired, adminOnly, (req, res) => {
  upload.single('file')(req, res, (error) => {
    if (error instanceof multer.MulterError) {
      if (error.code === 'LIMIT_FILE_SIZE') {
        return res.status(400).json({ message: 'File too large (max 5MB image)' });
      }
      return res.status(400).json({ message: 'Only image files are allowed' });
    }
    if (error) {
      console.error('Image upload failed:', error);
      return res.status(500).json({ message: 'File upload failed, please try again later' });
    }
    if (!req.file) {
      return res.status(400).json({ message: 'No file detected' });
    }

    const publicPath = `/uploads/${req.file.filename}`;
    const origin = `${req.protocol}://${req.get('host')}`;
    res.status(201).json({
      path: publicPath,
      url: `${origin}${publicPath}`,
      size: req.file.size,
      mimeType: req.file.mimetype,
    });
  });
});

app.post('/api/auth/register', async (req, res) => {
  const { email, password, name } = req.body ?? {};
  if (!email || !password || !name) {
    return res.status(400).json({ message: 'Missing required fields' });
  }

  const exists = await query('SELECT id FROM users WHERE email = :email LIMIT 1', { email });
  if (exists.length) {
    return res.status(409).json({ message: 'Email already registered' });
  }

  const user = {
    id: nanoid(),
    email,
    name,
    role: 'user',
  };

  await execute(
    `INSERT INTO users (id, email, name, role, password_hash)
       VALUES (:id, :email, :name, :role, :passwordHash)`,
    { ...user, passwordHash: await bcrypt.hash(password, 10) },
  );

  await saveUserProfile(user.id, {});

  const token = issueToken(user);
  res.status(201).json({ token, user });
});

app.post('/api/auth/login', async (req, res) => {
  const { email, password } = req.body ?? {};
  if (!email || !password) {
    return res.status(400).json({ message: 'Missing required fields' });
  }

  const rows = await query(
    'SELECT id, email, name, role, password_hash AS passwordHash FROM users WHERE email = :email LIMIT 1',
    { email },
  );
  if (!rows.length) {
    return res.status(401).json({ message: 'Invalid email or password' });
  }

  const user = rows[0];
  const match = await bcrypt.compare(password, user.passwordHash);
  if (!match) {
    return res.status(401).json({ message: 'Invalid email or password' });
  }

  const sanitized = sanitizeUser(user);
  const token = issueToken(sanitized);
  res.json({ token, user: sanitized });
});

app.get('/api/auth/profile', authRequired, async (req, res) => {
  const rows = await query('SELECT id, email, name, role FROM users WHERE id = :id LIMIT 1', { id: req.user.sub });
  if (!rows.length) {
    return res.status(404).json({ message: 'User not found' });
  }
  res.json(rows[0]);
});

app.get('/api/profile', authRequired, async (req, res) => {
  const profile = await ensureUserProfile(req.user.sub);
  res.json(profile);
});

app.patch('/api/profile', authRequired, async (req, res) => {
  try {
    const payload = req.body ?? {};
    const profile = await saveUserProfile(req.user.sub, {
      height: payload.height,
      weight: payload.weight,
      gender: payload.gender,
      age: payload.age,
      goal: payload.goal,
    });
    res.json(profile);
  } catch (error) {
    console.error('Profile update failed:', error);
    res.status(500).json({ message: '保存失败，请稍后再试' });
  }
});

app.get('/api/dashboard', async (_req, res) => {
  try {
    const [articles, recipes, guides] = await Promise.all([
      fetchArticles(),
      fetchRecipes(),
      fetchGuides(),
    ]);

    res.json({ articles, recipes, guides });
  } catch (error) {
    console.error('Dashboard fetch failed:', error);
    res.status(500).json({ message: 'Service temporarily unavailable, please try again later' });
  }
});

app.get('/api/recipes', async (_req, res) => {
  const recipes = await fetchRecipes();
  res.json(recipes);
});

app.post('/api/recipes', authRequired, adminOnly, async (req, res) => {
  try {
    const { name, calories, image, ingredients = [], method = '', tags = [] } = req.body ?? {};
    const preparedIngredients = prepareIngredients(ingredients);
    if (!name || !calories || !preparedIngredients.length || !method.trim()) {
      return res.status(400).json({ message: 'Missing required fields' });
    }

    const recipeId = nanoid();
    const normalizedTags = ensureArray(tags).map((tag) => String(tag).trim()).filter(Boolean);

    await execute(
      `INSERT INTO recipes (id, name, calories, image, ingredients, method)
         VALUES (:id, :name, :calories, :image, :ingredients, :method)`,
      {
        id: recipeId,
        name,
        calories: Number(calories),
        image: image ?? '',
        ingredients: JSON.stringify(preparedIngredients),
        method: method.trim(),
      },
    );

    if (normalizedTags.length) {
      const values = normalizedTags.map((tag) => ({ recipe_id: recipeId, tag }));
      for (const value of values) {
        await execute('INSERT INTO recipe_tags (recipe_id, tag) VALUES (:recipe_id, :tag)', value);
      }
    }

    const recipe = (await fetchRecipes()).find((item) => item.id === recipeId);
    res.status(201).json(recipe);
  } catch (error) {
    console.error('Recipe creation failed:', error);
    res.status(500).json({ message: 'Failed to create recipe', error: error.message });
  }
});

app.put('/api/recipes/:id', authRequired, adminOnly, async (req, res) => {
  const { id } = req.params;
  const rows = await query('SELECT id FROM recipes WHERE id = :id', { id });
  if (!rows.length) {
    return res.status(404).json({ message: 'Recipe not found' });
  }

  const tags = ensureArray(req.body?.tags).map((tag) => String(tag).trim()).filter(Boolean);
  const ingredients = req.body?.ingredients !== undefined ? prepareIngredients(req.body.ingredients) : undefined;

  await transaction(async (conn) => {
    await conn.execute(
      `UPDATE recipes SET
         name = COALESCE(:name, name),
         calories = COALESCE(:calories, calories),
         image = COALESCE(:image, image),
         ingredients = COALESCE(:ingredients, ingredients),
         method = COALESCE(:method, method)
       WHERE id = :id`,
      {
        id,
        name: req.body?.name,
        calories: req.body?.calories !== undefined ? Number(req.body.calories) : undefined,
        image: req.body?.image,
        ingredients: ingredients !== undefined ? JSON.stringify(ingredients) : undefined,
        method: req.body?.method ?? undefined,
      },
    );

    if (req.body?.tags !== undefined) {
      await conn.execute('DELETE FROM recipe_tags WHERE recipe_id = :id', { id });
      for (const tag of tags) {
        await conn.execute('INSERT INTO recipe_tags (recipe_id, tag) VALUES (:recipe_id, :tag)', {
          recipe_id: id,
          tag,
        });
      }
    }
  });

  const updated = (await fetchRecipes()).find((recipe) => recipe.id === id);
  res.json(updated);
});

app.delete('/api/recipes/:id', authRequired, adminOnly, async (req, res) => {
  const { id } = req.params;
  await transaction(async (conn) => {
    await conn.execute('DELETE FROM recipe_tags WHERE recipe_id = :id', { id });
    await conn.execute('DELETE FROM recipes WHERE id = :id', { id });
  });
  res.status(204).send();
});

app.get('/api/guides', async (_req, res) => {
  const guides = await fetchGuides();
  res.json(guides);
});

app.post('/api/guides', authRequired, adminOnly, async (req, res) => {
  const { title, icon = 'guide', image, tips = [] } = req.body ?? {};
  if (!title || !image) {
    return res.status(400).json({ message: 'Missing required fields' });
  }

  const guide = {
    id: nanoid(),
    title,
    icon,
    image: String(image),
    tips: normalizeTips(tips),
  };

  await execute(
    `INSERT INTO guides (id, title, icon, image, tips)
       VALUES (:id, :title, :icon, :image, :tips)`,
    { ...guide, tips: JSON.stringify(guide.tips) },
  );

  res.status(201).json(guide);
});

app.put('/api/guides/:id', authRequired, adminOnly, async (req, res) => {
  const { id } = req.params;
  const rows = await query('SELECT id FROM guides WHERE id = :id', { id });
  if (!rows.length) {
    return res.status(404).json({ message: 'Guide not found' });
  }

  await execute(
    `UPDATE guides SET
       title = COALESCE(:title, title),
       icon = COALESCE(:icon, icon),
       image = COALESCE(:image, image),
       tips = COALESCE(:tips, tips)
     WHERE id = :id`,
    {
      id,
      title: req.body?.title,
      icon: req.body?.icon,
      image: req.body?.image,
      tips: req.body?.tips !== undefined ? JSON.stringify(normalizeTips(req.body.tips)) : undefined,
    },
  );

  const updated = (await fetchGuides()).find((guide) => guide.id === id);
  res.json(updated);
});

app.delete('/api/guides/:id', authRequired, adminOnly, async (req, res) => {
  const { id } = req.params;
  const result = await execute('DELETE FROM guides WHERE id = :id', { id });
  if (!result.affectedRows) {
    return res.status(404).json({ message: 'Guide not found' });
  }
  res.status(204).send();
});

app.get('/api/users', authRequired, adminOnly, async (_req, res) => {
  const users = await query(
    'SELECT id, email, name, role, DATE_FORMAT(created_at, "%Y-%m-%d %H:%i:%s") AS createdAt FROM users ORDER BY created_at DESC',
  );
  res.json(users.map(sanitizeUser));
});

app.patch('/api/users/:id', authRequired, adminOnly, async (req, res) => {
  const { id } = req.params;
  const rows = await query('SELECT id FROM users WHERE id = :id', { id });
  if (!rows.length) {
    return res.status(404).json({ message: 'User not found' });
  }

  await execute(
    `UPDATE users SET
       name = COALESCE(:name, name),
       role = COALESCE(:role, role)
     WHERE id = :id`,
    {
      id,
      name: req.body?.name === undefined ? null : req.body.name,
      role: req.body?.role === undefined ? null : req.body.role,
    },
  );

  const updated = await query('SELECT id, email, name, role FROM users WHERE id = :id', { id });
  res.json(updated[0]);
});

app.delete('/api/users/:id', authRequired, adminOnly, async (req, res) => {
  const { id } = req.params;
  if (id === req.user.sub) {
    return res.status(400).json({ message: '无法删除当前登录账户' });
  }
  const result = await execute('DELETE FROM users WHERE id = :id', { id });
  if (!result.affectedRows) {
    return res.status(404).json({ message: 'User not found' });
  }
  res.status(204).send();
});

app.get('/api/health-profiles', authRequired, adminOnly, async (_req, res) => {
  const rows = await query(
    `SELECT
       u.id AS userId, u.email, u.name, u.role,
       p.height, p.weight, p.gender, p.age, p.bmi, p.bmr, p.goal,
       DATE_FORMAT(p.updated_at, "%Y-%m-%d %H:%i:%s") AS updatedAt
     FROM users u
     LEFT JOIN user_profiles p ON p.user_id = u.id
     ORDER BY u.created_at DESC`,
  );
  res.json(rows);
});

app.put('/api/health-profiles/:userId', authRequired, adminOnly, async (req, res) => {
  const { userId } = req.params;
  const rows = await query('SELECT id FROM users WHERE id = :id', { id: userId });
  if (!rows.length) {
    return res.status(404).json({ message: 'User not found' });
  }
  try {
    const payload = req.body ?? {};
    const profile = await saveUserProfile(userId, {
      height: payload.height,
      weight: payload.weight,
      gender: payload.gender,
      age: payload.age,
      goal: payload.goal,
    });
    res.json(profile);
  } catch (error) {
    console.error('Admin update profile failed:', error);
    res.status(500).json({ message: '保存失败，请稍后再试' });
  }
});

app.get('/api/articles/:id', async (req, res) => {
  const { id } = req.params;
  const rows = await query(
    `SELECT id, title, summary, body, cover_image AS coverImage, category,
            DATE_FORMAT(created_at, "%Y-%m-%d %H:%i:%s") AS createdAt
       FROM articles WHERE id = :id`,
    { id },
  );
  if (!rows.length) {
    return res.status(404).json({ message: 'Article not found' });
  }
  res.json(rows[0]);
});

app.listen(port, () => {
  console.log(`Mint Health API (MySQL) running on http://localhost:${port}`);
});
